<?php


namespace EchoPHP\Pipeline;

use Closure;
use RuntimeException;
use EchoPHP\Foundation\Container;

class Pipeline {
    /**
     * 容器实例
     * @var Container
     */
    protected $container;

    /**
     * 通过管道处理的对象实例
     * @var
     */
    protected $passable;

    /**
     * 管道处理类
     * @var
     */
    protected $pipes;

    /**
     * 管道中处理对象的方法
     * @var string
     */
    protected $method = 'handle';

    public function __construct(Container $container = null) {
        $this->container = $container;
    }

    /**
     * 设置通过管道处理的对象实例
     * @param $passable
     * @return $this
     */
    public function send($passable) {
        $this->passable = $passable;
        return $this;
    }

    /**
     * 设置处理管道
     * @param $pipes
     * @return $this
     */
    public function through($pipes) {
        $this->pipes = is_array($pipes) ? $pipes : func_get_args();
        return $this;
    }

    /**
     * 设置管道中的对象处理方法
     * @param string $method
     * @return $this
     */
    public function via($method = 'handle') {
        $this->method = $method;
        return $this;
    }

    /**
     * 执行管道处理逻辑，实质就是按管道顺序封装嵌套为多重闭包，然后执行这个嵌套闭包
     * p1（前置管道） p2(后置管道) destination 封装结果为，所谓前置或者后置就是看在destination之前还是之后执行
     * f1（param）{
     *      p1 业务逻辑
     *      f2(param){
     *          fd (param){ real destination  业务处理逻辑 }
     *          p2 业务处理逻辑
     *      }
     * }
     * 管道类核心逻辑
     *  重点1： array_reduce 将回调函数 callback 迭代地作用到 array 数组中的每一个单元中，从而将数组简化为单一的值
     *          array_reduce ( array $array , callable $callback [, mixed $initial = NULL ] ) : mixed
     *              callback(mixed $carry, mixed $item)
     *                  carry  携带上次迭代里的值，如果本次是迭代的第一次，那么这个值就是 initial
     *                  item  携带了本次迭代的值
     *              initial 如果指定了可选参数 initial，该参数将在处理开始前使用，或者当处理结束，数组为空时的最后一个结果
     * @param Closure $destination 目标回调，
     * @return mixed
     */
    public function then(Closure $destination) {
        $pipeline = array_reduce(
            array_reverse($this->pipes), $this->carry(), $this->prepareDestination($destination)
        );

        return $pipeline($this->passable);
    }

    /**
     * 业务处理逻辑封装为相应闭包
     * @param Closure $destination
     * @return Closure
     */
    protected function prepareDestination(Closure $destination) {
        return function ($passable) use ($destination) {
            return $destination($passable);
        };
    }

    /**
     * 将相关管道封装为闭包
     * @return Closure
     */
    protected function carry() {
        //作为array_reduce的第二个参数匿名回调函数，需要两个参数，  第一个参数值是上次迭代的值，如果是首次迭代就是initial  第二个参数是本次迭代的值
        //也就是说，首次执行的时候 stack 就是目标逻辑处理代码段 $destination， pipe 就是第一个执行管道逻辑
        //后续执行时，stack就是上次执行的闭包， pipe就是当前执行到的管道逻辑
        //最后一次执行时， stack时上次执行的闭包,也就是倒数第二个执行管道逻辑产生的闭包， pipe时最后一个执行管道逻辑。随后 stack闭包会在then方法中进行调用
        return function ($stack, $pipe) {
            return function ($passable) use ($stack, $pipe) {
                if (is_callable($pipe)) {
                    return $pipe($passable, $stack);
                } elseif (!is_object($pipe)) {
                    [$name, $parameters] = $this->parsePipeString($pipe);
                    $pipe = $this->getContainer()->make($name);
                    $parameters = array_merge([$passable, $stack], $parameters);
                } else {
                    $parameters = [$passable, $stack];
                }
                if (method_exists($pipe, $this->method)) {
                    $response = $pipe->{$this->method}(...$parameters);
                } else {
                    $response = $pipe(...$parameters);
                }

                return $response;
            };
        };
    }

    /**
     * 解析传递的管道字符串
     * @param $pipe
     * @return array
     */
    protected function parsePipeString($pipe) {
        [$name, $parameters] = array_pad(explode(':', $pipe, 2), 2, []);
        if (is_string($parameters)) {
            $parameters = explode(',', $parameters);
        }

        return [$name, $parameters];
    }

    /**
     * 获取管道用的容器实例
     * @return Container
     */
    protected function getContainer() {
        if (!$this->container) {
            throw new RuntimeException('pipeline 尚未设置容器实例');
        }
        return $this->container;
    }
}